package ca.kooki.codesnippet;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import ca.kooki.codesnippet.core.Action;
import ca.kooki.codesnippet.core.ActionResponse;
import ca.kooki.codesnippet.core.FileAccessor;
import ca.kooki.codesnippet.core.SearchInput;
import ca.kooki.codesnippet.core.SystemDate;
import ca.kooki.codesnippet.core.User;
import ca.kooki.codesnippet.core.XmlBuffer;

public class FileSystemCodeSnippetRepository implements CodeSnippetRepository {

  
  //```````````````````````
  // STATIC VARIABLES
  //```````````````````````

  private static final String FILE_ENCODING = "8859_1";

  //```````````````````````
  // MEMBER VARIABLES
  //```````````````````````

  private CodeSnippetRepositoryHelper _helper;
  private String _codeSnippetFilename;
  private String _usersFilename;
  private String _searchesFilename;
  private String _actionsFilename;
  
  private ActionResponse _problemsToDate;
  
  //```````````````````````
  // CONSTRUCTOR
  //```````````````````````

  public FileSystemCodeSnippetRepository(String filename)
  {
    init(filename);
  }
  
  private void init(String filename)
  {
    _codeSnippetFilename = filename;
    _usersFilename = filename + ".users";
    _searchesFilename = filename + ".searches";
    _actionsFilename = filename + ".actions";
    _helper = new CodeSnippetRepositoryHelper();
    _problemsToDate = new ActionResponse();
    _problemsToDate.setSuccessMessage("repository_configured_properly");
    
    createDirectoryStructureAsRequired();
  }
  
  //```````````````````````
  // INTERFACE
  //```````````````````````
  
  public User getUser()
  {
    return _helper.getUser();
  }
  
  public void setUser(User user)
  {
    _helper.setUser(user);
  }
  
  public void setUser(String username)
  {
    setUser(getUserByUsername(username));
  }
  
  public ActionResponse addCodeSnippet(CodeSnippet snippet) 
  {

    ActionResponse response = new ActionResponse();
    Action action = _helper.actionForNewCodeSnippet(snippet);

    if (snippet.getCode().trim().equals(""))
    {
      response.addError("no_code_snippet");
      return addActionBasedOnResponse(response, action);
    }


    try 
    {   
      Document doc = readDocument(_codeSnippetFilename);
      String currentId = extractIdFrom(doc);
      
      XmlBuffer buffer = new XmlBuffer();

      buffer.addEntity("all-code-snippets");
      buffer.addSimpleEntity("next-id", nextId(currentId) + "");
      
      CodeSnippet[] oldSnippets = getAllSnippets();
      for (int i=0; i<oldSnippets.length; i++)
      {
        CodeSnippet existingSnippet = oldSnippets[i];
        if (existingSnippet.isSameCodeAs(snippet))
        {
          snippet.setId(existingSnippet.getId());
          return response;
        }
        addSnippetTo(buffer,existingSnippet);
      }
      snippet.setId(currentId);
      addSnippetTo(buffer, snippet);
      buffer.closeEntity();
      
      writeFileToDisk(_codeSnippetFilename,buffer);
    }
    catch (Exception e) 
    {
      response.addError(e.getMessage());
    }
    
    return addActionBasedOnResponse(response, action);
  }
  
  public ActionResponse addUser(User user)
  {
    ActionResponse response = new ActionResponse();
    Action action = _helper.actionForNewUser(user);
    
    if (user.getName().trim().equals(""))
    {
      response.addError("empty_user_name");
      return addActionBasedOnResponse(response,action);
    }
    
    try 
    {   

      Document doc = readDocument(_usersFilename);
      String currentId = extractIdFrom(doc);
      
      User[] oldUsers = getAllUsers();
      
      XmlBuffer buffer = new XmlBuffer();

      buffer.addEntity("all-users");
      buffer.addSimpleEntity("next-id", nextId(currentId) + "");
      
      for (int i=0; i<oldUsers.length; i++)
      {
        User existing = oldUsers[i];
        if (existing.getName().equals(user.getName()))
        {
          user.setId(existing.getId());
          response.addError("existing_user_name");
          return response;
        }
        addUserTo(buffer,existing);
      }
      user.setId(currentId);
      addUserTo(buffer, user);
      buffer.closeEntity();

      writeFileToDisk(_usersFilename,buffer);
      
    }
    catch (Exception e) 
    {
      response.addError(e.getMessage());
    }
    
    return addActionBasedOnResponse(response,action);
  }
  
  public ActionResponse addTag(Tag tag)
  {
    ActionResponse response = new ActionResponse();
    Action action = _helper.actionForNewTag(tag);
    
    if (tag.getUser().getName() == "")
    {
      response.addError("no_user_for_tag");
      return addActionBasedOnResponse(response, action);
    }
    addUser(tag.getUser());
    response.merge(addCodeSnippet(tag.getCodeSnippet()));
    
    if (!response.isValid())
    {
      return addActionBasedOnResponse(response, action);
    }
    
    String filename = _codeSnippetFilename + "." + tag.getUser().getId();
    
    try 
    {   

      Document doc = readDocument(filename);
      int currentId = new Integer(extractIdFrom(doc)).intValue();
      int nextId = tag.isOfficial() ? currentId : currentId + 1;
      Tag[] oldTags = getAllTags("/" + tag.getUser().getName(),false);
      
      XmlBuffer buffer = new XmlBuffer();

      buffer.addEntity("all-tags");
      buffer.addSimpleEntity("next-id", nextId + "");
      
      for (int i=0; i<oldTags.length; i++)
      {
        Tag existing = oldTags[i];
        if (tag.isOfficial() && existing.getId().equals(tag.getId()))
        {
          addTagTo(buffer,tag);
        }
        else
        {
          addTagTo(buffer,existing);  
        }
      }
      
      if (!tag.isOfficial())
      {
        tag.setId(currentId + "");
        addTagTo(buffer, tag);
      }
      buffer.closeEntity();
      writeFileToDisk(filename,buffer);
    }
    catch (Exception e) 
    {
      response.addError(e.getMessage());
    }

    return addActionBasedOnResponse(response, action);
  }

  public CodeSnippet[] getAllSnippets() 
  {
    
    try 
    {
      Document doc = readDocument(_codeSnippetFilename);
      
      if (doc == null)
      {
        return new CodeSnippet[0];  
      }
      
      NodeList allXmlSnippets = doc.getElementsByTagName("code-snippet");
      
      ArrayList<CodeSnippet> allSnippets = new ArrayList<CodeSnippet>();
      
      for(int i=0; i<allXmlSnippets.getLength() ; i++)
      {
        Node xmlSnippet = allXmlSnippets.item(i);
        CodeSnippet snippet = extractCodeSnippetFrom(xmlSnippet);
        allSnippets.add(snippet);
      }
      return (CodeSnippet[])allSnippets.toArray(new CodeSnippet[allSnippets.size()]);
    } 
    catch (Exception e) 
    {
      return new CodeSnippet[0];
    }
  }
  
  public SearchInput[] getAllSearches(String searchQuery)
  {
    
    try 
    {
      Document doc = readDocument(_searchesFilename);
      
      if (doc == null)
      {
        return new SearchInput[0];  
      }
      
      NodeList allXmlSearches = doc.getElementsByTagName("search-input");
      
      ArrayList<SearchInput> allSearches = new ArrayList<SearchInput>();
      
      for(int i=0; i<allXmlSearches.getLength() ; i++)
      {
        Node xmlCode = allXmlSearches.item(i);
        SearchInput input = extractSearchInputFrom(xmlCode);
        allSearches.add(input);
      }
      return (SearchInput[])allSearches.toArray(new SearchInput[allSearches.size()]);
    } 
    catch (Exception e) 
    {
      return new SearchInput[0];
    }
  }
  
  public User getUserByUsername(String username)
  {
    if (username == null)
    {
      username = "";
    }

    User[] allUsers = getAllUsers();
    for (int i=0; i<allUsers.length; i++)
    {
      User potentialMatch = allUsers[i];
      if (potentialMatch.getName().equals(username))
      {
        return potentialMatch;
      }
    }
    return new User(username);
  }
  
  public User[] getAllUsers()
  {
    try 
    {
      Document doc = readDocument(_usersFilename);
      
      if (doc == null)
      {
        return new User[0];  
      }
      
      NodeList allXmlUsers = doc.getElementsByTagName("user");
      
      ArrayList<User> allUsers = new ArrayList<User>();
      
      for(int i=0; i<allXmlUsers.getLength() ; i++)
      {
        Node xmlCode = allXmlUsers.item(i);
        User user = extractUserFrom(xmlCode);
        allUsers.add(user);
      }
      return (User[])allUsers.toArray(new User[allUsers.size()]);
    } 
    catch (Exception e) 
    {
      return new User[0];
    }
  }
  
  public Tag[] getAllTags(String searchQuery)
  {
    return getAllTags(searchQuery,searchQuery != null);
  }
  
  
  public Action[] getAllActions()
  {
    try 
    {
      Document doc = readDocument(_actionsFilename);
      
      if (doc == null)
      {
        return new Action[0];  
      }
      
      NodeList allXmlActions = doc.getElementsByTagName("action");
      
      ArrayList<Action> allActions = new ArrayList<Action>();
      
      for(int i=0; i<allXmlActions.getLength() ; i++)
      {
        Node xmlAction = allXmlActions.item(i);
        Action action = extractActionFrom(xmlAction);
        allActions.add(action);
      }
      return (Action[])allActions.toArray(new Action[allActions.size()]);
    } 
    catch (Exception e) 
    {
      return new Action[0];
    }

  }  

  public boolean isUser(String username)
  {
    return getUser(username).isOfficial();
  }
  
  public StatisticSummary getStatistics()
  {
    StatisticSummary summary = new StatisticSummary();
    Tag[] allTags = getAllTags(null);
    for (int i=0; i<allTags.length; i++)
    {
      summary.addTag(allTags[i]);
    }
    return summary;
  }
  
  public ActionResponse isValid()
  {
    init(_codeSnippetFilename);
    readDocument(_codeSnippetFilename);
    return _problemsToDate;
  }
  
  //```````````````````````
  // PRIVATE METHODS
  //```````````````````````
  
  private User getUser(String username)
  {
    User[] allUsers = getAllUsers();
    for (int i=0; i<allUsers.length; i++)
    {
      User potentialMatch = allUsers[i];
      if (potentialMatch.getName().equals(username))
      {
        return potentialMatch;
      }
    }
    return new User(username);
  }
  
  
  private Document readDocument(String filename)
  {
    try
    {
      FileAccessor accessor = new FileAccessor(filename);
      if (!accessor.lock())
      {
        return null;
      }
      File theDocument = new File(filename);
      Document doc = null;
      if (theDocument.exists())
      {
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        doc = docBuilder.parse(theDocument);
      }
      accessor.unlock();
      return doc;
    }
    catch (Exception e)
    {
      _problemsToDate.addError("problem_with_repository_file", new String[] {filename,e.getMessage()});
      return null;
    }
  }

  private ActionResponse addAction(Action action)
  {
    ActionResponse response = new ActionResponse();
    
    try 
    {   
      XmlBuffer buffer = new XmlBuffer();

      buffer.addEntity("all-actions");
      
      Action[] oldActions = getAllActions();
      for (int i=0; i<oldActions.length; i++)
      {
        Action existingAction = oldActions[i];
        addActionTo(buffer, existingAction);
      }
      addActionTo(buffer,action);
      buffer.closeEntity();
      
      writeFileToDisk(_actionsFilename,buffer);
    }
    catch (Exception e) 
    {
      response.addError("unable-to-save-search");
    }
      
    return response;
  }

  
  private ActionResponse addSearchInput(SearchInput input)
  {
    ActionResponse response = new ActionResponse();
    
    try 
    {   
      XmlBuffer buffer = new XmlBuffer();

      buffer.addEntity("all-search-inputs");
      
      SearchInput[] oldSearches = getAllSearches(null);
      for (int i=0; i<oldSearches.length; i++)
      {
        SearchInput existingSearch = oldSearches[i];
        addSearchInputTo(buffer, existingSearch);
      }
      addSearchInputTo(buffer,input);
      buffer.closeEntity();
      
      writeFileToDisk(_searchesFilename,buffer);
    }
    catch (Exception e) 
    {
      response.addError("unable-to-save-search");
    }
      
    return response;
  }
  
  private Action extractActionFrom(Node xmlNode)
  {
    NamedNodeMap allAttributes = xmlNode.getAttributes();
    
    
    String type = allAttributes.getNamedItem("type").getTextContent();
    String description = allAttributes.getNamedItem("description").getTextContent();

    Date when;
    try {
      when = SystemDate.parse(allAttributes.getNamedItem("when").getTextContent());
    } catch (DOMException e) {
      when = SystemDate.Now();
    } catch (ParseException e) {
      when = SystemDate.Now();
    }
    
    boolean success = allAttributes.getNamedItem("success").getTextContent().equals("true");
    String userId = allAttributes.getNamedItem("user-id").getTextContent();
    String userName = allAttributes.getNamedItem("user-name").getTextContent();
    
    User by = new User(userId,userName);
    
    Action action = new Action(type,description);
    action.setDate(when);
    action.setSuccess(success);
    action.setUser(by);
    return action;
  }
  
  private Tag extractTagFrom(Node xmlNode)
  {
    NamedNodeMap allAttributes = xmlNode.getAttributes();
    
    String id = allAttributes.getNamedItem("id").getTextContent();
    String title = allAttributes.getNamedItem("title").getTextContent();
    String keywords = allAttributes.getNamedItem("keywords").getTextContent();
    String lastUpdated = allAttributes.getNamedItem("updated").getTextContent();
    
    NodeList snippetAndUserNode = xmlNode.getChildNodes();
    CodeSnippet snippet = extractCodeSnippetFrom(snippetAndUserNode.item(1));
    User user = extractUserFrom(snippetAndUserNode.item(3));
    
    Tag tag = new Tag(title,keywords,snippet,user);
    try {
      tag.setUpdated(SystemDate.parse(lastUpdated));
    } catch (ParseException e) {
    }
    tag.setId(id);
    return tag;
  }
  
  private CodeSnippet extractCodeSnippetFrom(Node xmlNode)
  {
    NamedNodeMap allAttributes = xmlNode.getAttributes();
    
    String id = allAttributes.getNamedItem("id").getTextContent();
    String title = allAttributes.getNamedItem("title").getTextContent();
    String keywords = allAttributes.getNamedItem("keywords").getTextContent();
    String code = xmlNode.getTextContent();
    
    CodeSnippet snippet = new CodeSnippet(title,keywords,code);
    snippet.setId(id);
    return snippet;
  }
  
  private SearchInput extractSearchInputFrom(Node xmlNode)
  {
    NamedNodeMap allAttributes = xmlNode.getAttributes();
    
    String userId = allAttributes.getNamedItem("user-id").getTextContent();
    String name = allAttributes.getNamedItem("user-name").getTextContent();
    String query = allAttributes.getNamedItem("text").getTextContent();
    
    User user = new User(name);
    user.setId(userId);
    
    SearchInput input = new SearchInput(user,query);
    return input;
  }
  
  private User extractUserFrom(Node xmlNode)
  {
    NamedNodeMap allAttributes = xmlNode.getAttributes();
    
    String id = allAttributes.getNamedItem("id").getTextContent();
    String name = allAttributes.getNamedItem("name").getTextContent();
    
    User user = new User(name);
    user.setId(id);
    return user;
  }

  private void addSnippetTo(XmlBuffer buffer, CodeSnippet snippet)
  {
    buffer.addEntity("code-snippet");
    buffer.addAttribute("id", snippet.getId());
    buffer.addAttribute("title", snippet.getTitle());
    buffer.addAttribute("keywords",snippet.getKeywords());
    buffer.addInnerXml(snippet.getCode());
    buffer.closeEntity();
  }
  
  private void addUserTo(XmlBuffer buffer, User user)
  {
    buffer.addEntity("user");
    buffer.addAttribute("id", user.getId());
    buffer.addAttribute("name", user.getName());
    buffer.closeEntity();
  }
  
  private void addTagTo(XmlBuffer buffer, Tag tag)
  {
    buffer.addEntity("tag");
    buffer.addAttribute("id", tag.getId());
    buffer.addAttribute("title", tag.getTitle());
    buffer.addAttribute("keywords",tag.getKeywords());
    buffer.addAttribute("updated",SystemDate.formatDate(tag.getUpdated()));
    addSnippetTo(buffer, tag.getCodeSnippet());
    addUserTo(buffer, tag.getUser());
    buffer.closeEntity();
  }
  
  private void addActionTo(XmlBuffer buffer, Action action)
  {
    buffer.addEntity("action");
    buffer.addAttribute("user-id", action.getUser().getId());
    buffer.addAttribute("user-name", action.getUser().getName());
    buffer.addAttribute("type", action.getType());
    buffer.addAttribute("description", action.getDescription());
    buffer.addAttribute("when", SystemDate.formatDate(action.when()));
    buffer.addAttribute("success", action.getSuccessText());
    buffer.closeEntity();
  }
  
  private void addSearchInputTo(XmlBuffer buffer, SearchInput input)
  {
    buffer.addEntity("search-input");
    buffer.addAttribute("user-id", input.from().getId());
    buffer.addAttribute("user-name", input.from().getName());
    buffer.addAttribute("text", input.getSearchQuery());
    buffer.closeEntity();
  }
  
  private void writeFileToDisk(String filename, XmlBuffer buffer) throws IOException
  {
    OutputStream fout = new FileOutputStream(filename);
    OutputStream bout = new BufferedOutputStream(fout);
    OutputStreamWriter out = new OutputStreamWriter(bout, FILE_ENCODING);

    out.write(buffer.toString());
    out.flush();
    out.close();
  }
  
  private String extractIdFrom(Document doc)
  {
    if (doc == null)
    {
      return "1";
    }
    else
    {
      Node nextIdNode = doc.getElementsByTagName("next-id").item(0);
      return nextIdNode.getTextContent().trim();
    }
  }
  
  private int nextId(String currentId)
  {
    return new Integer(currentId).intValue() + 1;
  }

  private boolean mergeAsRequired(List<Tag> masterList, List<Tag> additionalElements, List<String> allSnippetsToDate)
  {
    boolean canMerge = additionalElements != null;
    if (canMerge)
    {
      for (int i=0; i<additionalElements.size(); i++)
      {
        Tag newTag = additionalElements.get(i);
        String codeSnippetId = newTag.getCodeSnippet().getId();
        if (allSnippetsToDate.contains(codeSnippetId))
        {
          continue;
        }
        allSnippetsToDate.add(codeSnippetId);
        masterList.add(newTag);
      }
    }
    return canMerge;
  }
  
  private List<Tag> extractTagsFrom(String filename, SearchInput input)
  {
    try 
    {
      
      Document doc = readDocument(filename);
      
      if (doc == null)
      {
        return null;  
      }
      
      NodeList allXmlTags = doc.getElementsByTagName("tag");
      
      ArrayList<Tag> allTags = new ArrayList<Tag>();
      
      for(int i=0; i<allXmlTags.getLength(); i++)
      {
        Node xmlTag = allXmlTags.item(i);
        Tag tag = extractTagFrom(xmlTag);
        if (tag.isRelevant(input))
        {
          allTags.add(tag);  
        }
      }
      return allTags;
    } 
    catch (Exception e) 
    {
      e.printStackTrace();
      return new ArrayList<Tag>();
    }

  }
  
  private ActionResponse addActionBasedOnResponse(ActionResponse response, Action action)
  {
    action.setSuccess(response.isValid());
    addAction(action);
    return response;
  }

  private Tag[] getAllTags(String searchQuery, boolean shouldSaveAction)
  {
    SearchInput input = new SearchInput(getUser(),searchQuery);

    List<String> allSnippetsToDate = new ArrayList<String>();
    ArrayList<Tag> allTags = new ArrayList<Tag>();
    if (input.hasUsername())
    {
      User user = getUser(input.getUsername());
      if (user.isOfficial())
      {
        String filename = _codeSnippetFilename + "." + user.getId();
        List<Tag> additionalTags = extractTagsFrom(filename,input);
        mergeAsRequired(allTags,additionalTags,allSnippetsToDate);
      }
    }
    else
    {
      
      User[] allUsers = getAllUsers();
      
      for(int i=0; i<allUsers.length; i++)
      {
        User user = allUsers[i];
        String filename = _codeSnippetFilename + "." + user.getId();
        List<Tag> additionalTags = extractTagsFrom(filename,input);
        mergeAsRequired(allTags,additionalTags,allSnippetsToDate);
      }
    }
    
    if (shouldSaveAction)
    {
      addSearchInput(input);
      Action action = _helper.actionForNewSeach(searchQuery,allTags.size());
      addAction(action);
    }

    return (Tag[])allTags.toArray(new Tag[allTags.size()]);
  }
  
  private void createDirectoryStructureAsRequired()
  {
    File directory = new File(_codeSnippetFilename).getParentFile();
    
    if (directory != null && !directory.exists())
    {
      boolean wasSaved = directory.mkdirs();
      if (!wasSaved)
      {
        _problemsToDate.addError("unable_create_repository_directory",new String[] {directory.getAbsolutePath()});
      }
    }
  }


  
}
